using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using PaulRz.Util.Patterns;

namespace PaulRz.Util.Threading.ThreadedLogger
{
    /// <summary>
    /// This delegate is used to define if the logger should be changed due to log rotation
    /// </summary>
    /// <param name="sender"></param>
    public delegate void LogRotate(Logger sender);

    /// <summary>
    /// Thread-separated lazy logger with lock-free addition of new records to log.
    /// Writing to the writer occurs if High-Water Mark (HWM) or timeout is exceeded
    /// </summary>
    public sealed class Logger : ActiveObject, IDisposable
    {
        private bool isDisposed = false;
        
        ///<summary>
        ///Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        ///</summary>
        ///<filterpriority>2</filterpriority>
        public void Dispose()
        {
            isDisposed = true;

            if (flushEvent!=null)
            {
                flushEvent.Close();
                flushEvent = null;
            }

            if (acceptBuffers!=null)
            {
                acceptBuffers.Clear();
                acceptBuffers = null;
            }

            if (flushBuffers!=null)
            {
                flushBuffers.Clear();
                flushBuffers = null;
            }
        }

        private TextWriter writer;
        private int HWM;
        private int Timeout;

        /// <summary>
        /// Create a logger.
        /// </summary>
        /// <param name="writer">Underlying writer</param>
        /// <param name="hwm">High-Water Mark of the number of logs to store in memory before storing</param>
        /// <param name="timeout"></param>
        public Logger(TextWriter writer, int hwm, int timeout) : base("Logger")
        {
            this.writer = writer;
            HWM = hwm;
            Timeout = timeout;
        }

        private object SyncRoot = new object();

        private Dictionary<int, List<LogEntry>> acceptBuffers =
            new Dictionary<int, List<LogEntry>>(50);

        private Dictionary<int, List<LogEntry>> flushBuffers =
            new Dictionary<int, List<LogEntry>>(50);

        private ManualResetEvent flushEvent = new ManualResetEvent(false);

        /// <summary>
        /// Changes the old writer to new one.
        /// The old writer is flushed and closed.
        /// </summary>
        /// <param name="newWriter"></param>
        public void ChangeWriter(TextWriter newWriter)
        {
            if (newWriter == null)
                throw new ArgumentNullException("newWriter", "LogWriter is null!");
            
            writer.Flush();
            writer.Close();
            writer = newWriter;
        }

        /// <summary>
        /// The event is trigged after each storing
        /// </summary>
        public event LogRotate OnLogRotate = null;

        /// <summary>
        /// Thread-safe LogString function
        /// </summary>
        /// <param name="logStr"></param>
        public void LogString(string logStr)
        {
            if (isDisposed)
                throw new ObjectDisposedException("Logger has been disposed");

            // this should separate the threads as soon as this id is unique for thread
            // this helps us to avoid locks
            int threadId = Thread.CurrentThread.ManagedThreadId;

            List<LogEntry> buffToAccept;

            try
            {
                buffToAccept = acceptBuffers[threadId];
            }
            catch(KeyNotFoundException)
            {
                buffToAccept = new List<LogEntry>(100);
                acceptBuffers[threadId] = buffToAccept;
            }

            // As soon as this buffer is intended for usage only by one thread - this operation is thread-safe
            buffToAccept.Add(new LogEntry(logStr));

            if (buffToAccept.Count > HWM)
                flushEvent.Set();
        }

        private void exchangeQueues<T>(ref T a, ref T b)
            where T:class 
        {
            lock (SyncRoot)
                b = Interlocked.Exchange(ref a, b);
        }

        /// <summary>
        /// Stops the logger main thread
        /// </summary>
        public override void Stop()
        {
            flushEvent.Set();
            base.Stop();
        }

        protected override void Run()
        {
            while(!Stopped)
            {
                try
                {
                    flushEvent.WaitOne(Timeout, false);
                    exchangeQueues(ref acceptBuffers, ref flushBuffers);

                    List<LogEntry> list = new List<LogEntry>(flushBuffers.Count*HWM);
                    lock (SyncRoot) 
                    {
                        foreach (KeyValuePair<int, List<LogEntry>> pair in flushBuffers)
                        {
                            list.AddRange(pair.Value);
                        }
                        flushBuffers.Clear();
                    }
                    list.Sort();
                    write(list);

                    if (OnLogRotate != null)
                        OnLogRotate(this);
                }
                catch(ThreadAbortException)
                {
                    return;
                }
                finally
                {
                    writer.Flush();
                    flushEvent.Reset();
                }
            }
        }

        private void write(List<LogEntry> list)
        {
            foreach (LogEntry val in list)
            {
                if (val.record!=null)
                    writer.WriteLine(val.record);
            }
        }

        private class LogEntry : IComparable<LogEntry>
        {
            public LogEntry(string record)
            {
                this.record = record;
            }

            public DateTime timestamp = DateTime.Now;
            public string record;


            ///<summary>
            ///Compares the current object with another object of the same type.
            ///</summary>
            ///
            ///<returns>
            ///A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the other parameter.Zero This object is equal to other. Greater than zero This object is greater than other. 
            ///</returns>
            ///
            ///<param name="other">An object to compare with this object.</param>
            public int CompareTo(LogEntry other)
            {
                if (other != null)
                    return timestamp.CompareTo(other.timestamp);
                else
                    return -1;
            }
        }
    }
}
